/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.sanselan.formats.gif;

import java.awt.Dimension;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.sanselan.FormatCompliance;
import org.apache.sanselan.ImageFormat;
import org.apache.sanselan.ImageInfo;
import org.apache.sanselan.ImageParser;
import org.apache.sanselan.ImageReadException;
import org.apache.sanselan.ImageWriteException;
import org.apache.sanselan.common.BinaryOutputStream;
import org.apache.sanselan.common.IImageMetadata;
import org.apache.sanselan.common.byteSources.ByteSource;
import org.apache.sanselan.common.mylzw.MyLZWCompressor;
import org.apache.sanselan.common.mylzw.MyLZWDecompressor;
import org.apache.sanselan.palette.Palette;
import org.apache.sanselan.palette.PaletteFactory;
import org.apache.sanselan.util.Debug;
import org.apache.sanselan.util.ParamMap;

public class GifImageParser extends ImageParser
{

	public GifImageParser()
	{
		super.setByteOrder(BYTE_ORDER_LSB);
	}

	public String getName()
	{
		return "Gif-Custom";
	}

	public String getDefaultExtension()
	{
		return DEFAULT_EXTENSION;
	}

	private static final String DEFAULT_EXTENSION = ".gif";

	private static final String ACCEPTED_EXTENSIONS[] = { DEFAULT_EXTENSION, };

	protected String[] getAcceptedExtensions()
	{
		return ACCEPTED_EXTENSIONS;
	}

	protected ImageFormat[] getAcceptedTypes()
	{
		return new ImageFormat[] { ImageFormat.IMAGE_FORMAT_GIF, //
		};
	}

	private static final byte GIF_HEADER_SIGNATURE[] = { 71, 73, 70 };

	private GIFHeaderInfo readHeader(InputStream is,
			FormatCompliance formatCompliance) throws ImageReadException,
			IOException
	{
		byte identifier1 = readByte("identifier1", is, "Not a Valid GIF File");
		byte identifier2 = readByte("identifier2", is, "Not a Valid GIF File");
		byte identifier3 = readByte("identifier3", is, "Not a Valid GIF File");

		byte version1 = readByte("version1", is, "Not a Valid GIF File");
		byte version2 = readByte("version2", is, "Not a Valid GIF File");
		byte version3 = readByte("version3", is, "Not a Valid GIF File");

		if (formatCompliance != null)
		{
			formatCompliance.compare_bytes("Signature", GIF_HEADER_SIGNATURE,
					new byte[] { identifier1, identifier2, identifier3, });
			formatCompliance.compare("version", 56, version1);
			formatCompliance
					.compare("version", new int[] { 55, 57, }, version2);
			formatCompliance.compare("version", 97, version3);
		}

		if (debug)
			printCharQuad("identifier: ", ((identifier1 << 16)
					| (identifier2 << 8) | (identifier3 << 0)));
		if (debug)
			printCharQuad("version: ",
					((version1 << 16) | (version2 << 8) | (version3 << 0)));

		int logicalScreenWidth = read2Bytes("Logical Screen Width", is,
				"Not a Valid GIF File");
		int logicalScreenHeight = read2Bytes("Logical Screen Height", is,
				"Not a Valid GIF File");

		if (formatCompliance != null)
		{
			formatCompliance.checkBounds("Width", 1, Integer.MAX_VALUE,
					logicalScreenWidth);
			formatCompliance.checkBounds("Height", 1, Integer.MAX_VALUE,
					logicalScreenHeight);
		}

		byte packedFields = readByte("Packed Fields", is,
				"Not a Valid GIF File");
		byte backgroundColorIndex = readByte("Background Color Index", is,
				"Not a Valid GIF File");
		byte pixelAspectRatio = readByte("Pixel Aspect Ratio", is,
				"Not a Valid GIF File");

		if (debug)
			printByteBits("PackedFields bits", packedFields);

		boolean globalColorTableFlag = ((packedFields & 128) > 0);
		if (debug)
			System.out.println("GlobalColorTableFlag: " + globalColorTableFlag);
		byte colorResolution = (byte) ((packedFields >> 4) & 7);
		if (debug)
			System.out.println("ColorResolution: " + colorResolution);
		boolean sortFlag = ((packedFields & 8) > 0);
		if (debug)
			System.out.println("SortFlag: " + sortFlag);
		byte sizeofGlobalColorTable = (byte) (packedFields & 7);
		if (debug)
			System.out.println("SizeofGlobalColorTable: "
					+ sizeofGlobalColorTable);

		if (formatCompliance != null)
		{
			if (globalColorTableFlag && backgroundColorIndex != -1)
				formatCompliance.checkBounds("Background Color Index", 0,
						convertColorTableSize(sizeofGlobalColorTable),
						backgroundColorIndex);
		}

		return new GIFHeaderInfo(identifier1, identifier2, identifier3,
				version1, version2, version3, logicalScreenWidth,
				logicalScreenHeight, packedFields, backgroundColorIndex,
				pixelAspectRatio, globalColorTableFlag, colorResolution,
				sortFlag, sizeofGlobalColorTable);
	}

	private GraphicControlExtension readGraphicControlExtension(int code,
			InputStream is) throws ImageReadException, IOException
	{
		readByte("block_size", is, "GIF: corrupt GraphicControlExt");
		int packed = readByte("packed fields", is,
				"GIF: corrupt GraphicControlExt");

		int dispose = (packed & 0x1c) >> 2; // disposal method
		boolean transparency = (packed & 1) != 0;

		int delay = read2Bytes("delay in milliseconds", is,
				"GIF: corrupt GraphicControlExt");
		int transparentColorIndex = 0xff & readByte("transparent color index",
				is, "GIF: corrupt GraphicControlExt");
		readByte("block terminator", is, "GIF: corrupt GraphicControlExt");

		return new GraphicControlExtension(code, packed, dispose, transparency,
				delay, transparentColorIndex);
	}

	private byte[] readSubBlock(InputStream is) throws ImageReadException,
			IOException
	{
		int block_size = 0xff & readByte("block_size", is, "GIF: corrupt block");

		byte bytes[] = readByteArray("block", block_size, is,
				"GIF: corrupt block");

		return bytes;
	}

	protected GenericGIFBlock readGenericGIFBlock(InputStream is, int code)
			throws ImageReadException, IOException
	{
		return readGenericGIFBlock(is, code, null);
	}

	protected GenericGIFBlock readGenericGIFBlock(InputStream is, int code,
			byte first[]) throws ImageReadException, IOException
	{
		ArrayList subblocks = new ArrayList();

		if (first != null)
			subblocks.add(first);

		while (true)
		{
			byte bytes[] = readSubBlock(is);
			if (bytes.length < 1)
				break;
			subblocks.add(bytes);
		}

		return new GenericGIFBlock(code, subblocks);
	}

	private final static int EXTENSION_CODE = 0x21;
	private final static int IMAGE_SEPARATOR = 0x2C;
	private final static int GRAPHIC_CONTROL_EXTENSION = (EXTENSION_CODE << 8) | 0xf9;
	private final static int COMMENT_EXTENSION = 0xfe;
	private final static int PLAIN_TEXT_EXTENSION = 0x01;
	private final static int XMP_EXTENSION = 0xff;
	private final static int TERMINATOR_BYTE = 0x3b;
	private final static int APPLICATION_EXTENSION_LABEL = 0xff;
	private final static int XMP_COMPLETE_CODE = (EXTENSION_CODE << 8)
			| XMP_EXTENSION;

	private ArrayList readBlocks(GIFHeaderInfo ghi, InputStream is,
			boolean stopBeforeImageData, FormatCompliance formatCompliance)
			throws ImageReadException, IOException
	{
		ArrayList result = new ArrayList();

		while (true)
		{
			int code = is.read();
			// this.debugNumber("code: ", code);

			switch (code)
			{
			case -1:
				throw new ImageReadException("GIF: unexpected end of data");

			case IMAGE_SEPARATOR:
				ImageDescriptor id = readImageDescriptor(ghi, code, is,
						stopBeforeImageData, formatCompliance);
				result.add(id);
				// if(stopBeforeImageData)
				// return result;

				break;

			case EXTENSION_CODE: // extension
			{
				int extensionCode = is.read();
				// this.debugNumber("extension_code: ", extension_code);
				int completeCode = ((0xff & code) << 8)
						| (0xff & extensionCode);

				switch (extensionCode)
				{
				case 0xf9:
					GraphicControlExtension gce = readGraphicControlExtension(
							completeCode, is);
					result.add(gce);
					break;

				case COMMENT_EXTENSION:
				case PLAIN_TEXT_EXTENSION: {
					GenericGIFBlock block = readGenericGIFBlock(is,
							completeCode);
					result.add(block);
					break;
				}

				case APPLICATION_EXTENSION_LABEL: // 255 (hex 0xFF) Application
					// Extension Label
				{
					byte label[] = readSubBlock(is);

					if (formatCompliance != null)
						formatCompliance
								.addComment("Unknown Application Extension ("
										+ new String(label) + ")", completeCode);

					// if (label == new String("ICCRGBG1"))
					{
						// GIF's can have embedded ICC Profiles - who knew?
					}

					if ((label != null) && (label.length > 0))
					{
						GenericGIFBlock block = readGenericGIFBlock(is,
								completeCode, label);
						byte bytes[] = block.appendSubBlocks();

						result.add(block);
					}
					break;
				}

				default: {

					if (formatCompliance != null)
						formatCompliance.addComment("Unknown block",
								completeCode);

					GenericGIFBlock block = readGenericGIFBlock(is,
							completeCode);
					result.add(block);
					break;
				}
				}
			}
				break;

			case TERMINATOR_BYTE:
				return result;

			case 0x00: // bad byte, but keep going and see what happens
				break;

			default:
				throw new ImageReadException("GIF: unknown code: " + code);
			}
		}
	}

	private ImageDescriptor readImageDescriptor(GIFHeaderInfo ghi,
			int blockCode, InputStream is, boolean stopBeforeImageData,
			FormatCompliance formatCompliance) throws ImageReadException,
			IOException
	{
		int ImageLeftPosition = read2Bytes("Image Left Position", is,
				"Not a Valid GIF File");
		int ImageTopPosition = read2Bytes("Image Top Position", is,
				"Not a Valid GIF File");
		int imageWidth = read2Bytes("Image Width", is, "Not a Valid GIF File");
		int imageHeight = read2Bytes("Image Height", is, "Not a Valid GIF File");
		byte PackedFields = readByte("Packed Fields", is,
				"Not a Valid GIF File");

		if (formatCompliance != null)
		{
			formatCompliance.checkBounds("Width", 1, ghi.logicalScreenWidth,
					imageWidth);
			formatCompliance.checkBounds("Height", 1, ghi.logicalScreenHeight,
					imageHeight);
			formatCompliance.checkBounds("Left Position", 0,
					ghi.logicalScreenWidth - imageWidth, ImageLeftPosition);
			formatCompliance.checkBounds("Top Position", 0,
					ghi.logicalScreenHeight - imageHeight, ImageTopPosition);
		}

		if (debug)
			printByteBits("PackedFields bits", PackedFields);

		boolean LocalColorTableFlag = (((PackedFields >> 7) & 1) > 0);
		if (debug)
			System.out.println("LocalColorTableFlag: " + LocalColorTableFlag);
		boolean InterlaceFlag = (((PackedFields >> 6) & 1) > 0);
		if (debug)
			System.out.println("Interlace Flag: " + InterlaceFlag);
		boolean SortFlag = (((PackedFields >> 5) & 1) > 0);
		if (debug)
			System.out.println("Sort  Flag: " + SortFlag);

		byte SizeofLocalColorTable = (byte) (PackedFields & 7);
		if (debug)
			System.out.println("SizeofLocalColorTable: "
					+ SizeofLocalColorTable);

		byte LocalColorTable[] = null;
		if (LocalColorTableFlag)
			LocalColorTable = readColorTable(is, SizeofLocalColorTable,
					formatCompliance);

		byte imageData[] = null;
		if (!stopBeforeImageData)
		{
			int LZWMinimumCodeSize = is.read();

			GenericGIFBlock block = readGenericGIFBlock(is, -1);
			byte bytes[] = block.appendSubBlocks();
			InputStream bais = new ByteArrayInputStream(bytes);

			int size = imageWidth * imageHeight;
			MyLZWDecompressor myLzwDecompressor = new MyLZWDecompressor(
					LZWMinimumCodeSize, BYTE_ORDER_LSB);
			imageData = myLzwDecompressor.decompress(bais, size);
		} else
		{
			int LZWMinimumCodeSize = is.read();
			if (debug)
				System.out.println("LZWMinimumCodeSize: " + LZWMinimumCodeSize);

			readGenericGIFBlock(is, -1);
		}

		ImageDescriptor result = new ImageDescriptor(blockCode,
				ImageLeftPosition, ImageTopPosition, imageWidth, imageHeight,
				PackedFields, LocalColorTableFlag, InterlaceFlag, SortFlag,
				SizeofLocalColorTable, LocalColorTable, imageData);

		return result;
	}

	private int simple_pow(int base, int power)
	{
		int result = 1;

		for (int i = 0; i < power; i++)
			result *= base;

		return result;
	}

	private int convertColorTableSize(int ct_size)
	{
		return 3 * simple_pow(2, ct_size + 1);
	}

	private byte[] readColorTable(InputStream is, int ct_size,
			FormatCompliance formatCompliance) throws ImageReadException,
			IOException
	{
		int actual_size = convertColorTableSize(ct_size);

		byte bytes[] = readByteArray("block", actual_size, is,
				"GIF: corrupt Color Table");

		return bytes;
	}

	private GIFHeaderInfo readHeader(ByteSource byteSource)
			throws ImageReadException, IOException
	{
		InputStream is = null;
		try
		{
			is = byteSource.getInputStream();

			return readHeader(is, FormatCompliance.getDefault());
		} finally
		{
			try
			{
				is.close();
			} catch (Exception e)
			{
				Debug.debug(e);
			}

		}
	}

	private GIFBlock findBlock(ArrayList v, int code)
	{
		for (int i = 0; i < v.size(); i++)
		{
			GIFBlock gifBlock = (GIFBlock) v.get(i);
			if (gifBlock.blockCode == code)
				return gifBlock;
		}
		return null;
	}

	private ImageContents readFile(ByteSource byteSource,
			boolean stopBeforeImageData) throws ImageReadException, IOException
	{
		return readFile(byteSource, stopBeforeImageData, FormatCompliance
				.getDefault());
	}

	private ImageContents readFile(ByteSource byteSource,
			boolean stopBeforeImageData, FormatCompliance formatCompliance)
			throws ImageReadException, IOException
	{
		InputStream is = null;
		try
		{
			is = byteSource.getInputStream();

			GIFHeaderInfo ghi = readHeader(is, formatCompliance);

			byte globalColorTable[] = null;
			if (ghi.globalColorTableFlag)
				globalColorTable = readColorTable(is,
						ghi.sizeOfGlobalColorTable, formatCompliance);

			ArrayList blocks = readBlocks(ghi, is, stopBeforeImageData,
					formatCompliance);

			ImageContents result = new ImageContents(ghi, globalColorTable,
					blocks);

			return result;
		} finally
		{
			try
			{
				// bis.close();
				is.close();
			} catch (Exception e)
			{
				Debug.debug(e);
			}

		}
	}

	public byte[] getICCProfileBytes(ByteSource byteSource, Map params)
			throws ImageReadException, IOException
	{
		return null;
	}

	public Dimension getImageSize(ByteSource byteSource, Map params)
			throws ImageReadException, IOException
	{
		GIFHeaderInfo bhi = readHeader(byteSource);

		if (bhi == null)
			throw new ImageReadException("GIF: Couldn't read Header");

		return new Dimension(bhi.logicalScreenWidth, bhi.logicalScreenHeight);

	}

	public byte[] embedICCProfile(byte image[], byte profile[])
	{
		return null;
	}

	public boolean embedICCProfile(File src, File dst, byte profile[])
	{
		return false;
	}

	public IImageMetadata getMetadata(ByteSource byteSource, Map params)
			throws ImageReadException, IOException
	{
		return null;
	}

	private ArrayList getComments(ArrayList v) throws ImageReadException,
			IOException
	{
		ArrayList result = new ArrayList();
		int code = 0x21fe;

		for (int i = 0; i < v.size(); i++)
		{
			GIFBlock block = (GIFBlock) v.get(i);
			if (block.blockCode == code)
			{
				byte bytes[] = ((GenericGIFBlock) block).appendSubBlocks();
				result.add(new String(bytes));
			}
		}

		return result;
	}

	public ImageInfo getImageInfo(ByteSource byteSource, Map params)
			throws ImageReadException, IOException
	{
		ImageContents blocks = readFile(byteSource, false);

		if (blocks == null)
			throw new ImageReadException("GIF: Couldn't read blocks");

		GIFHeaderInfo bhi = blocks.gifHeaderInfo;
		if (bhi == null)
			throw new ImageReadException("GIF: Couldn't read Header");

		ImageDescriptor id = (ImageDescriptor) findBlock(blocks.blocks,
				IMAGE_SEPARATOR);
		if (id == null)
			throw new ImageReadException("GIF: Couldn't read ImageDescriptor");

		GraphicControlExtension gce = (GraphicControlExtension) findBlock(
				blocks.blocks, GRAPHIC_CONTROL_EXTENSION);

		int Height = bhi.logicalScreenHeight;
		int Width = bhi.logicalScreenWidth;

		ArrayList Comments;

		Comments = getComments(blocks.blocks);

		int BitsPerPixel = (bhi.colorResolution + 1) * 3;
		ImageFormat Format = ImageFormat.IMAGE_FORMAT_GIF;
		String FormatName = "GIF Graphics Interchange Format";
		String MimeType = "image/gif";
		// we ought to count images, but don't yet.
		int NumberOfImages = -1;

		boolean isProgressive = id.interlaceFlag;

		int PhysicalWidthDpi = 72;
		float PhysicalWidthInch = (float) ((double) Width / (double) PhysicalWidthDpi);
		int PhysicalHeightDpi = 72;
		float PhysicalHeightInch = (float) ((double) Height / (double) PhysicalHeightDpi);

		String FormatDetails = "Gif " + ((char) blocks.gifHeaderInfo.version1)
				+ ((char) blocks.gifHeaderInfo.version2)
				+ ((char) blocks.gifHeaderInfo.version3);

		boolean isTransparent = false;
		if (gce != null && gce.transparency)
			isTransparent = true;

		boolean usesPalette = true;
		int colorType = ImageInfo.COLOR_TYPE_RGB;
		String compressionAlgorithm = ImageInfo.COMPRESSION_ALGORITHM_LZW;

		ImageInfo result = new ImageInfo(FormatDetails, BitsPerPixel, Comments,
				Format, FormatName, Height, MimeType, NumberOfImages,
				PhysicalHeightDpi, PhysicalHeightInch, PhysicalWidthDpi,
				PhysicalWidthInch, Width, isProgressive, isTransparent,
				usesPalette, colorType, compressionAlgorithm);

		return result;
	}

	public boolean dumpImageFile(PrintWriter pw, ByteSource byteSource)
			throws ImageReadException, IOException
	{
		pw.println("gif.dumpImageFile");

		{
			ImageInfo imageData = getImageInfo(byteSource);
			if (imageData == null)
				return false;

			imageData.toString(pw, "");
		}
		{
			ImageContents blocks = readFile(byteSource, false);

			if (blocks == null)
				return false;

			pw.println("gif.blocks: " + blocks.blocks.size());
			for (int i = 0; i < blocks.blocks.size(); i++)
			{
				GIFBlock gifBlock = (GIFBlock) blocks.blocks.get(i);
				this.debugNumber(pw, "\t" + i + " ("
						+ gifBlock.getClass().getName() + ")",
						gifBlock.blockCode, 4);
			}

		}

		pw.println("");

		return true;
	}

	private int[] getColorTable(byte bytes[]) throws ImageReadException,
			IOException
	{
		if ((bytes.length % 3) != 0)
			throw new ImageReadException("Bad Color Table Length: "
					+ bytes.length);
		int length = bytes.length / 3;

		int result[] = new int[length];

		for (int i = 0; i < length; i++)
		{
			int red = 0xff & bytes[(i * 3) + 0];
			int green = 0xff & bytes[(i * 3) + 1];
			int blue = 0xff & bytes[(i * 3) + 2];

			int alpha = 0xff;

			int rgb = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0);
			result[i] = rgb;
		}

		return result;
	}

	public FormatCompliance getFormatCompliance(ByteSource byteSource)
			throws ImageReadException, IOException
	{
		FormatCompliance result = new FormatCompliance(byteSource
				.getDescription());

		readFile(byteSource, false, result);

		return result;
	}

	public BufferedImage getBufferedImage(ByteSource byteSource, Map params)
			throws ImageReadException, IOException
	{
		ImageContents imageContents = readFile(byteSource, false);

		if (imageContents == null)
			throw new ImageReadException("GIF: Couldn't read blocks");

		GIFHeaderInfo ghi = imageContents.gifHeaderInfo;
		if (ghi == null)
			throw new ImageReadException("GIF: Couldn't read Header");

		ImageDescriptor id = (ImageDescriptor) findBlock(imageContents.blocks,
				IMAGE_SEPARATOR);
		if (id == null)
			throw new ImageReadException("GIF: Couldn't read Image Descriptor");
		GraphicControlExtension gce = (GraphicControlExtension) findBlock(
				imageContents.blocks, GRAPHIC_CONTROL_EXTENSION);

		int width = ghi.logicalScreenWidth;
		int height = ghi.logicalScreenHeight;

		boolean hasAlpha = false;
		if (gce != null && gce.transparency)
			hasAlpha = true;

		BufferedImage result = getBufferedImageFactory(params)
				.getColorBufferedImage(width, height, hasAlpha);

		{
			int colortable[];
			if (id.localColorTable != null)
				colortable = getColorTable(id.localColorTable);
			else if (imageContents.globalColorTable != null)
				colortable = getColorTable(imageContents.globalColorTable);
			else
				throw new ImageReadException("Gif: No Color Table");

			int transparentIndex = -1;
			if (hasAlpha)
				transparentIndex = gce.transparentColorIndex;

			// Debug.debug("charles TransparentIndex", TransparentIndex);

			int counter = 0;
			// ByteArrayInputStream bais = new
			// ByteArrayInputStream(id.ImageData);
			// for (int y = 0; y < height; y++)

			int rows_in_pass_1 = (height + 7) / 8;
			int rows_in_pass_2 = (height + 3) / 8;
			int rows_in_pass_3 = (height + 1) / 4;
			int rows_in_pass_4 = (height) / 2;

			DataBuffer db = result.getRaster().getDataBuffer();

			for (int row = 0; row < height; row++)
			{

				int y;
				if (id.interlaceFlag)
				{
					int the_row = row;
					if (the_row < rows_in_pass_1)
						y = the_row * 8;
					else
					{
						the_row -= rows_in_pass_1;
						if (the_row < (rows_in_pass_2))
							y = 4 + (the_row * 8);
						else
						{
							the_row -= rows_in_pass_2;
							if (the_row < (rows_in_pass_3))
								y = 2 + (the_row * 4);
							else
							{
								the_row -= rows_in_pass_3;
								if (the_row < (rows_in_pass_4))
									y = 1 + (the_row * 2);
								else
									throw new ImageReadException(
											"Gif: Strange Row");
							}
						}
					}
					// System.out.println("row(" + row + "): " + y);
				} else
					y = row;

				for (int x = 0; x < width; x++)
				{

					int index = 0xff & id.imageData[counter++];
					int rgb = colortable[index];

					if (transparentIndex == index)
						rgb = 0x00;

					db.setElem(y * width + x, rgb);
				}

			}
		}

		return result;

	}

	private void writeAsSubBlocks(OutputStream os, byte bytes[])
			throws IOException
	{
		int index = 0;

		while (index < bytes.length)
		{
			int block_size = Math.min(bytes.length - index, 255);
			os.write(block_size);
			os.write(bytes, index, block_size);
			index += block_size;
		}
		os.write(0); // last block
	}

	private static final int LOCAL_COLOR_TABLE_FLAG_MASK = 1 << 7;
	private static final int INTERLACE_FLAG_MASK = 1 << 6;
	private static final int SORT_FLAG_MASK = 1 << 5;

	public void writeImage(BufferedImage src, OutputStream os, Map params)
			throws ImageWriteException, IOException
	{
		// make copy of params; we'll clear keys as we consume them.
		params = new HashMap(params);

		boolean verbose = ParamMap.getParamBoolean(params, PARAM_KEY_VERBOSE,
				false);

		// clear format key.
		if (params.containsKey(PARAM_KEY_FORMAT))
			params.remove(PARAM_KEY_FORMAT);
		if (params.containsKey(PARAM_KEY_VERBOSE))
			params.remove(PARAM_KEY_VERBOSE);

		String xmpXml = null;
		if (params.containsKey(PARAM_KEY_XMP_XML))
		{
			xmpXml = (String) params.get(PARAM_KEY_XMP_XML);
			params.remove(PARAM_KEY_XMP_XML);
		}

		if (params.size() > 0)
		{
			Object firstKey = params.keySet().iterator().next();
			throw new ImageWriteException("Unknown parameter: " + firstKey);
		}

		int width = src.getWidth();
		int height = src.getHeight();

		boolean hasAlpha = new PaletteFactory().hasTransparency(src);

		int max_colors = hasAlpha ? 255 : 256;

		Palette palette2 = new PaletteFactory().makePaletteSimple(src,
				max_colors);
		// int palette[] = new PaletteFactory().makePaletteSimple(src, 256);
		// Map palette_map = paletteToMap(palette);

		if (palette2 == null)
		{
			palette2 = new PaletteFactory().makePaletteQuantized(src,
					max_colors);
			if (verbose)
				System.out.println("quantizing");
		} else if (verbose)
			System.out.println("exact palette");

		if (palette2 == null)
			throw new ImageWriteException(
					"Gif: can't write images with more than 256 colors");
		int palette_size = palette2.length() + (hasAlpha ? 1 : 0);

		BinaryOutputStream bos = new BinaryOutputStream(os, BYTE_ORDER_LSB);

		{
			// write Header
			os.write(0x47); // G magic numbers
			os.write(0x49); // I
			os.write(0x46); // F

			os.write(0x38); // 8 version magic numbers
			os.write(0x39); // 9
			os.write(0x61); // a

			// Logical Screen Descriptor.

			bos.write2Bytes(width);
			bos.write2Bytes(height);

			int colorTableScaleLessOne = (palette_size > 128) ? 7
					: (palette_size > 64) ? 6 : (palette_size > 32) ? 5
							: (palette_size > 16) ? 4 : (palette_size > 8) ? 3
									: (palette_size > 4) ? 2
											: (palette_size > 2) ? 1 : 0;

			int colorTableSizeInFormat = 1 << (colorTableScaleLessOne + 1);
			int actual_size = 3 * simple_pow(2, colorTableScaleLessOne + 1);
			{
				byte colorResolution = (byte) colorTableScaleLessOne; // TODO:

				boolean globalColorTableFlag = false;
				boolean sortFlag = false;
				int globalColorTableFlagMask = 1 << 7;
				int sortFlagMask = 8;
				int sizeOfGlobalColorTable = 0;

				int packedFields = ((globalColorTableFlag ? globalColorTableFlagMask
						: 0)
						| (sortFlag ? sortFlagMask : 0)
						| ((7 & colorResolution) << 4) | (7 & sizeOfGlobalColorTable));
				bos.write(packedFields); // one byte
			}
			{
				byte BackgroundColorIndex = 0;
				bos.write(BackgroundColorIndex);
			}
			{
				byte PixelAspectRatio = 0;
				bos.write(PixelAspectRatio);
			}

			{ // write Global Color Table.

			}

			{ // ALWAYS write GraphicControlExtension
				bos.write(EXTENSION_CODE);
				bos.write((byte) 0xf9);
				// bos.write(0xff & (kGraphicControlExtension >> 8));
				// bos.write(0xff & (kGraphicControlExtension >> 0));

				bos.write((byte) 4); // block size;
				int packedFields = hasAlpha ? 1 : 0; // transparency flag
				bos.write((byte) packedFields);
				bos.write((byte) 0); // Delay Time
				bos.write((byte) 0); // Delay Time
				bos.write((byte) (hasAlpha ? palette2.length() : 0)); // Transparent
				// Color
				// Index
				bos.write((byte) 0); // terminator
			}

			if (null != xmpXml)
			{
				bos.write(EXTENSION_CODE);
				bos.write(APPLICATION_EXTENSION_LABEL);

				bos.write(XMP_APPLICATION_ID_AND_AUTH_CODE.length); // 0x0B
				bos.write(XMP_APPLICATION_ID_AND_AUTH_CODE);

				byte xmpXmlBytes[] = xmpXml.getBytes("utf-8");
				bos.write(xmpXmlBytes);

				// write "magic trailer"
				for (int magic = 0; magic <= 0xff; magic++)
					bos.write(0xff - magic);

				bos.write((byte) 0); // terminator

			}

			{ // Image Descriptor.
				bos.write(IMAGE_SEPARATOR);
				bos.write2Bytes(0); // Image Left Position
				bos.write2Bytes(0); // Image Top Position
				bos.write2Bytes(width); // Image Width
				bos.write2Bytes(height); // Image Height

				{
					boolean LocalColorTableFlag = true;
					// boolean LocalColorTableFlag = false;
					boolean InterlaceFlag = false;
					boolean SortFlag = false;
					int SizeOfLocalColorTable = colorTableScaleLessOne;

					// int SizeOfLocalColorTable = 0;

					int PackedFields = ((LocalColorTableFlag ? LOCAL_COLOR_TABLE_FLAG_MASK
							: 0)
							| (InterlaceFlag ? INTERLACE_FLAG_MASK : 0)
							| (SortFlag ? SORT_FLAG_MASK : 0) | (7 & SizeOfLocalColorTable));
					bos.write(PackedFields); // one byte
				}
			}

			{ // write Local Color Table.
				for (int i = 0; i < colorTableSizeInFormat; i++)
				{
					if (i < palette2.length())
					{
						int rgb = palette2.getEntry(i);

						int red = 0xff & (rgb >> 16);
						int green = 0xff & (rgb >> 8);
						int blue = 0xff & (rgb >> 0);

						bos.write(red);
						bos.write(green);
						bos.write(blue);
					} else
					{
						bos.write(0);
						bos.write(0);
						bos.write(0);
					}
				}
			}

			{ // get Image Data.
				int image_data_total = 0;

				int LZWMinimumCodeSize = colorTableScaleLessOne + 1;
//				LZWMinimumCodeSize = Math.max(8, LZWMinimumCodeSize);
				if (LZWMinimumCodeSize < 2)
					LZWMinimumCodeSize = 2;

				// TODO:
				// make
				// better
				// choice
				// here.
				bos.write(LZWMinimumCodeSize);

				MyLZWCompressor compressor = new MyLZWCompressor(
						LZWMinimumCodeSize, BYTE_ORDER_LSB, false); // GIF
				// Mode);

				byte imagedata[] = new byte[width * height];
				for (int y = 0; y < height; y++)
				{
					for (int x = 0; x < width; x++)
					{
						int argb = src.getRGB(x, y);
						int rgb = 0xffffff & argb;
						int index;

						if (hasAlpha)
						{
							int alpha = 0xff & (argb >> 24);
							final int alphaThreshold = 255;
							if (alpha < alphaThreshold)
								index = palette2.length(); // is transparent
							else
								index = palette2.getPaletteIndex(rgb);
						} else
						{
							index = palette2.getPaletteIndex(rgb);
						}

						imagedata[y * width + x] = (byte) index;
					}
				}

				byte compressed[] = compressor.compress(imagedata);
				writeAsSubBlocks(bos, compressed);
				image_data_total += compressed.length;
			}

			// palette2.dump();

			bos.write(TERMINATOR_BYTE);
		}

		bos.close();
		os.close();
	}

	private static final byte XMP_APPLICATION_ID_AND_AUTH_CODE[] = { 0x58, // X
			0x4D, // M
			0x50, // P
			0x20, //
			0x44, // D
			0x61, // a
			0x74, // t
			0x61, // a
			0x58, // X
			0x4D, // M
			0x50, // P
	};

	/**
	 * Extracts embedded XML metadata as XML string.
	 * <p>
	 * 
	 * @param file
	 *            File containing image data.
	 * @param params
	 *            Map of optional parameters, defined in SanselanConstants.
	 * @return Xmp Xml as String, if present. Otherwise, returns null..
	 */
	public String getXmpXml(ByteSource byteSource, Map params)
			throws ImageReadException, IOException
	{

		InputStream is = null;
		try
		{
			is = byteSource.getInputStream();

			FormatCompliance formatCompliance = null;
			GIFHeaderInfo ghi = readHeader(is, formatCompliance);

			byte globalColorTable[] = null;
			if (ghi.globalColorTableFlag)
				globalColorTable = readColorTable(is,
						ghi.sizeOfGlobalColorTable, formatCompliance);

			ArrayList blocks = readBlocks(ghi, is, true, formatCompliance);

			List result = new ArrayList();
			for (int i = 0; i < blocks.size(); i++)
			{
				GIFBlock block = (GIFBlock) blocks.get(i);
				if (block.blockCode != XMP_COMPLETE_CODE)
					continue;

				GenericGIFBlock genericBlock = (GenericGIFBlock) block;

				byte blockBytes[] = genericBlock.appendSubBlocks(true);
				if (blockBytes.length < XMP_APPLICATION_ID_AND_AUTH_CODE.length)
					continue;

				// this.debugByteArray("blockBytes", blockBytes);

				if (!compareByteArrays(blockBytes, 0,
						XMP_APPLICATION_ID_AND_AUTH_CODE, 0,
						XMP_APPLICATION_ID_AND_AUTH_CODE.length))
					continue;

				// this.debugByteArray("xmp block bytes", blockBytes);
				byte GIF_MAGIC_TRAILER[] = new byte[256];
				for (int magic = 0; magic <= 0xff; magic++)
					GIF_MAGIC_TRAILER[magic] = (byte) (0xff - magic);

				if (blockBytes.length < XMP_APPLICATION_ID_AND_AUTH_CODE.length
						+ GIF_MAGIC_TRAILER.length)
					continue;
				if (!compareByteArrays(blockBytes, blockBytes.length
						- GIF_MAGIC_TRAILER.length, GIF_MAGIC_TRAILER, 0,
						GIF_MAGIC_TRAILER.length))
					throw new ImageReadException(
							"XMP block in GIF missing magic trailer.");

				try
				{
					// XMP is UTF-8 encoded xml.
					String xml = new String(
							blockBytes,
							XMP_APPLICATION_ID_AND_AUTH_CODE.length,
							blockBytes.length
									- (XMP_APPLICATION_ID_AND_AUTH_CODE.length + GIF_MAGIC_TRAILER.length),
							"utf-8");
					result.add(xml);
				} catch (UnsupportedEncodingException e)
				{
					throw new ImageReadException("Invalid XMP Block in GIF.");
				}
			}

			if (result.size() < 1)
				return null;
			if (result.size() > 1)
				throw new ImageReadException("More than one XMP Block in GIF.");
			return (String) result.get(0);

		} finally
		{
			try
			{
				is.close();
			} catch (Exception e)
			{
				Debug.debug(e);
			}

		}
	}
}